﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using System.ComponentModel;
using System.Reflection;
using System.Linq.Expressions;
using UIT.Utils;
#if !SILVERLIGHT
using Microsoft.CSharp.RuntimeBinder;
#endif
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace UIT.Markup
{
#if !SILVERLIGHT
    [MarkupExtensionReturnType(typeof(object))]
#endif
    [ContentProperty("Path")]
    public class Binding : MarkupExtension, INotifyPropertyChanged, IDisposable
    {
        public Binding(BindingMode mode, string path, object source, string stringFormat)
        {
            Mode = mode;
            Path = path;
            Source = source;
            StringFormat = stringFormat;
        }

        public static TraceSource Trace = new TraceSource("UIT.Binding");

        public Binding()
        {
            Mode = BindingMode.TwoWay;
        }

        public Binding(string path)
        {
            this.Path = path;
            Mode = BindingMode.TwoWay;
        }

        private object source;

        public object Source
        {
            get { return source; }
            set
            {
                source = value;
                OnPropertyChanged("Source");
            }
        }


        private string path;

        public string Path
        {
            get { return path; }
            set
            {
                path = value;
                OnPropertyChanged("Path");
            }
        }


        public BindingMode Mode { get; set; }

        internal class BindingEvaluator : IDisposable
        {
            private readonly object target;
            private readonly PropertyInfo targetProperty;
            private readonly Binding binding;

            private BindingFragment initialFragment;

            public BindingEvaluator(Binding binding, object target, PropertyInfo targetProperty)
            {
                this.target = target;
                this.targetProperty = targetProperty;
                this.binding = binding;
                binding.PropertyChanged += new PropertyChangedEventHandler(binding_PropertyChanged);
            }

            public BindingEvaluator(Binding binding, INotifyPropertyChanged target, PropertyInfo targetProperty)
                : this(binding, (object)target, targetProperty)
            {
                if (binding.Mode == BindingMode.TwoWay)
                    target.PropertyChanged += new PropertyChangedEventHandler(target_PropertyChanged);
            }

            void target_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == targetProperty.Name)
                {
                    fragments[fragments.Count - 1].SetValue(GetValue(0, 1), targetProperty.GetValue(target, null));
                }
            }

            public BindingEvaluator(Binding binding, IDataContext target, PropertyInfo targetProperty)
                : this(binding, (INotifyPropertyChanged)target, targetProperty)
            {
                initialFragment = new BindingFragment(binding.Mode != BindingMode.OneTime);
                initialFragment.Member = "DataContext";
                initialFragment.PropertyChanged += new PropertyChangedEventHandler(fragment_PropertyChanged);
            }

            void binding_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == "Path")
                    targetProperty.SetValue(target, Convert(GetValue()), null);
            }

            private List<BindingFragment> fragments;

            public object GetValue(BindingFragment sourceFragment = null)
            {
                if (fragments == null)
                    BuildExpression(binding.Path);

                return GetValue(sourceFragment == null ? 0 : Math.Max(0, fragments.IndexOf(sourceFragment)), 0);
            }

            private object GetValue(int skip, int stop)
            {
                if (fragments == null)
                    BuildExpression(binding.Path);

                object value = target;
                for (int j = skip; j < fragments.Count - stop; j++)
                    value = fragments[j].GetValue(value);


                if (!string.IsNullOrEmpty(binding.StringFormat))
                    return string.Format(binding.StringFormat, value);
                return value;
            }

            private void BuildExpression(string path)
            {
                fragments = new List<BindingFragment>();
                if (initialFragment != null)
                    fragments.Add(initialFragment);
                BindingFragment fragment;
                if (!string.IsNullOrEmpty(path))
                {
                    string[] pathFragments = path.Split('.');

                    for (int i = 0; i < pathFragments.Length; i++)
                    {
                        fragment = new BindingFragment(binding.Mode != BindingMode.OneTime);
                        fragment.Member = pathFragments[i];
                        fragment.PropertyChanged += new PropertyChangedEventHandler(fragment_PropertyChanged);
                        fragments.Add(fragment);
                    }
                }
            }

            void fragment_PropertyChanged(object sender, PropertyChangedEventArgs e)
            {
                if (e.PropertyName == "Value")
                {
                    BindingFragment sourceFragment = ((BindingFragment)sender);
                    targetProperty.SetValue(target, Convert(GetValue()), null);
                }

            }


            private object Convert(object value)
            {
                return value.Convert(targetProperty);
            }

            #region IDisposable Members

            public void Dispose()
            {
                object target = this.target;
                foreach (var fragment in fragments)
                {
                    var newTarget = fragment.GetValue(target);
                    fragment.Dispose(target);
                    target = newTarget;
                }
            }

            #endregion

            internal void SetValue(object value)
            {
                targetProperty.SetValue(target, value, null);
            }
        }

        public void EnsureInit(INotifyPropertyChanged target, string targetPropertyName)
        {
            if (evaluator == null)
                Init(target, targetPropertyName);
        }
        public void EnsureInit(IDataContext target, string targetPropertyName)
        {
            if (evaluator == null)
                Init(target, targetPropertyName);
        }
        public void EnsureInit(INotifyPropertyChanged target, PropertyInfo targetProperty)
        {
            if (evaluator == null)
                Init(target, targetProperty);
        }
        public void EnsureInit(IDataContext target, PropertyInfo targetProperty)
        {
            if (evaluator == null)
                Init(target, targetProperty);
        }

        public void Init(INotifyPropertyChanged target, string targetPropertyName)
        {
            if (target == null)
                throw new ArgumentNullException("target");

            if (string.IsNullOrEmpty(targetPropertyName))
                throw new ArgumentNullException("targetPropertyName");

            Init(target, target.GetType().GetProperty(targetPropertyName));
        }
        public void Init(IDataContext target, string targetPropertyName)
        {
            if (target == null)
                throw new ArgumentNullException("target");

            if (string.IsNullOrEmpty(targetPropertyName))
                throw new ArgumentNullException("targetPropertyName");

            Init(target, target.GetType().GetProperty(targetPropertyName));
        }
        public void Init(INotifyPropertyChanged target, PropertyInfo targetProperty)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            if (targetProperty == null)
                throw new ArgumentNullException("targetProperty");

            evaluator = new BindingEvaluator(this, target, targetProperty);
        }
        public void Init(IDataContext target, PropertyInfo targetProperty)
        {
            if (target == null)
                throw new ArgumentNullException("target");
            if (targetProperty != null)
                throw new ArgumentNullException("targetProperty");

            evaluator = new BindingEvaluator(this, target, targetProperty);
        }

        BindingEvaluator evaluator;
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            if (serviceProvider == null)
                return this;
            if (evaluator == null)
            {
                IProvideValueTarget provider = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
                if (typeof(UIT.Controls.Template.ITemplate).IsAssignableFrom(((PropertyInfo)provider.TargetProperty).PropertyType))
                    return UIT.Converters.TemplateConverter.GetTemplate(this);
                INotifyPropertyChanged notifyer = provider.TargetObject as INotifyPropertyChanged;
                if (notifyer == null)
                    evaluator = new BindingEvaluator(this, provider.TargetObject, (PropertyInfo)provider.TargetProperty);
                else
                {
                    IBindingContainer bindingContainer = notifyer as IBindingContainer;
                    if (bindingContainer != null)
                        bindingContainer.SetBinding(((PropertyInfo)provider.TargetProperty).Name, this);
                    IDataContext dataContextNotifyer = notifyer as IDataContext;
                    if (dataContextNotifyer != null)
                        evaluator = new BindingEvaluator(this, dataContextNotifyer, (PropertyInfo)provider.TargetProperty);
                    else
                        evaluator = new BindingEvaluator(this, notifyer, (PropertyInfo)provider.TargetProperty);
                }
            }
            return evaluator.GetValue();
        }

        public string StringFormat { get; set; }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #endregion

        public void Dispose()
        {
            BindingEvaluator e = evaluator;
            evaluator = null;
            if (e != null)
                e.Dispose();
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append("{Binding ");
            sb.Append(Path);
            if (!string.IsNullOrEmpty(Path))
                sb.Append(", ");
            sb.Append("Mode=");
            sb.Append(Mode);
            if (!string.IsNullOrEmpty(StringFormat))
            {
                sb.Append(", StringFormat=");
                sb.Append(StringFormat);
            }
            sb.Append('}');
            return sb.ToString();
        }

        public object GetValue()
        {
            if (evaluator != null)
                return evaluator.GetValue();
            return null;
        }

        internal void SetValue(object value)
        {
            if (evaluator != null)
                evaluator.SetValue(value);
        }
    }
}
